package cn.geminis.lowcode.processor;

import cn.geminis.lowcode.annotation.CrudRepository;
import cn.geminis.lowcode.compiler.Context;
import cn.geminis.lowcode.compiler.code.*;
import cn.geminis.lowcode.compiler.util.JdkTreeMakerUtils;
import cn.geminis.lowcode.utils.Flags;
import cn.geminis.lowcode.utils.SourceFileContext;
import cn.geminis.lowcode.utils.Utils;

import javax.lang.model.element.Element;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * @author puddi
 */
public class CrudRepositoryProcessor implements BaseProcessor {

        @Override
        public Class<? extends Annotation> getSupportAnnotation() {
                return CrudRepository.class;
        }

        @Override
        public void process(Element element) {
                var annotation = element.getAnnotation(CrudRepository.class);
                var packageName = Utils.getElementPackageName(element);
                var repositoryPackageName = packageName + ".repository";
                var repositoryName = element.getSimpleName() + "Repository";
                var repositoryQualifiedName = repositoryPackageName + "." + repositoryName;
                var createClass = false;

                var entityClass = new LcClass(element);
                LcClass repository = SourceFileContext.findClass(repositoryQualifiedName);
                if (Objects.isNull(repository)) {
                        var existRepository = Context.INSTANCE.getElementUtils()
                                        .getTypeElement(repositoryPackageName + "." + repositoryName);
                        createClass = Objects.isNull(existRepository);
                        var entityName = packageName + "." + element.getSimpleName();

                        var entityIdType = entityClass.getFields().stream()
                                        .filter(field -> field.existAnnotation("jakarta.persistence.Id"))
                                        .findFirst()
                                        .orElseThrow(() -> new RuntimeException(
                                                        "Id field does not exist in " + entityName))
                                        .getType();
                        var jpaRepositoryImplement = "org.springframework.data.jpa.repository.JpaRepository<"
                                        + entityName + ", " + entityIdType + ">";
                        var jpaSpecificationImplement = "org.springframework.data.jpa.repository.JpaSpecificationExecutor<"
                                        + entityName + ">";
                        if (createClass) {
                                repository = new LcClass(
                                                LcPackage.ensure(repositoryPackageName),
                                                new LcModifier(
                                                                Flags.PUBLIC | Flags.INTERFACE),
                                                repositoryName,
                                                jpaRepositoryImplement,
                                                jpaSpecificationImplement);
                        } else {
                                repository = new LcClass(existRepository);
                                repository.appendImplementing(jpaRepositoryImplement);
                                repository.appendImplementing(jpaSpecificationImplement);
                        }
                        SourceFileContext.registerClass(repositoryQualifiedName, repository);
                }

                if (annotation.update()) {
                        addUpdateMethod(repository, element);
                }

                if (entityClass.existAnnotation("cn.geminis.lowcode.annotation.SoftDeleteEntity")) {
                        addSoftDeleteMethod(repository, element);
                }

                Utils.noteMessage(repositoryQualifiedName + (createClass ? " Generated" : " Enhanced"));
        }

        private void addUpdateMethod(LcClass repository, Element element) {
                var entityName = Utils.getElementQualifiedName(element);

                var body = new ArrayList<AbstractStatement<?>>();
                body.add(new LcVariable(
                                "jakarta.persistence.EntityManager",
                                "entityManager",
                                new LcApply("cn.hutool.extra.spring.SpringUtil.getBean",
                                                new LcIdent("jakarta.persistence.EntityManager.class")).getJdkTree()));
                body.add(new LcVariable(
                                "jakarta.persistence.criteria.CriteriaBuilder",
                                "criteriaBuilder",
                                new LcApply("entityManager.getCriteriaBuilder").getJdkTree()));
                body.add(new LcVariable(
                                "jakarta.persistence.criteria.CriteriaUpdate",
                                "criteriaUpdate",
                                new LcApply(
                                                "criteriaBuilder.createCriteriaUpdate",
                                                new LcIdent(entityName + ".class")).getJdkTree()));
                body.add(new LcVariable(
                                "jakarta.persistence.criteria.Root",
                                "root",
                                new LcApply(
                                                "criteriaUpdate.from",
                                                new LcIdent(entityName + ".class")).getJdkTree()));

                var entityClass = new LcClass(element);
                entityClass.getFields()
                                .stream().filter(field -> field.existAnnotation("jakarta.persistence.Column"))
                                .forEach(field -> {
                                        var getValueMethod = ("boolean".equals(field.getType()) ? "obj.is" : "obj.get")
                                                        + Utils.upperFirst(field.getName());
                                        body.add(
                                                        new LcIf(
                                                                        new LcUnary(
                                                                                        "NOT",
                                                                                        new LcApply(
                                                                                                        "java.util.Objects.isNull",
                                                                                                        new LcApply(getValueMethod))),
                                                                        List.of(
                                                                                        new LcApply(
                                                                                                        "criteriaUpdate.set",
                                                                                                        new LcLiteral(field
                                                                                                                        .getName()),
                                                                                                        new LcApply(getValueMethod))
                                                                                                        .exec())));
                                });

                var entityIdName = entityClass.getFields().stream()
                                .filter(field -> field.existAnnotation("jakarta.persistence.Id"))
                                .findFirst()
                                .orElseThrow(() -> new RuntimeException("Id field does not exist in " + entityName))
                                .getName();
                body.add(new LcApply(
                                "criteriaUpdate.where",
                                new LcApply(
                                                "criteriaBuilder.equal",
                                                new LcApply("root.get", new LcLiteral(entityIdName)),
                                                new LcTypeCast(
                                                                "Object",
                                                                new LcApply("obj.get"
                                                                                + Utils.upperFirst(entityIdName)))))
                                .exec());

                body.add(new LcVariable(
                                "jakarta.persistence.Query",
                                "query",
                                new LcApply(
                                                "entityManager.createQuery",
                                                new LcIdent("criteriaUpdate")).getJdkTree()));
                body.add(new LcApply("query.executeUpdate").rtn());

                var method = new LcMethod(
                                new LcModifier(
                                                Flags.DEFAULT,
                                                new LcAnnotation("org.springframework.data.jpa.repository.Modifying"),
                                                new LcAnnotation(
                                                                "jakarta.transaction.Transactional",
                                                                Map.of("rollbackOn",
                                                                                JdkTreeMakerUtils.select(
                                                                                                "Exception.class")))),
                                "INT",
                                "update",
                                List.of(
                                                new LcVariable(entityName, "obj")),
                                List.of(),
                                body);
                repository.prepend(method);
        }

        private void addSoftDeleteMethod(LcClass repository, Element element) {
                var entityName = element.getSimpleName().toString();
                var alisName = entityName.substring(0, 1).toLowerCase();

                Map.of("softDelete",
                                String.format("update %1$s %2$s set %2$s.deleted = true where %2$s.id in ?1",
                                                entityName, alisName),
                                "softUnDelete",
                                String.format("update %1$s %2$s set %2$s.deleted = false where %2$s.id in ?1",
                                                entityName, alisName))
                                .forEach((key, value) -> {
                                        var method = new LcMethod(
                                                        new LcModifier(
                                                                        Flags.InterfaceMethodFlags,
                                                                        new LcAnnotation(
                                                                                        "org.springframework.data.jpa.repository.Modifying"),
                                                                        new LcAnnotation(
                                                                                        "jakarta.transaction.Transactional",
                                                                                        Map.of("rollbackOn",
                                                                                                        JdkTreeMakerUtils
                                                                                                                        .select("Exception.class"))),
                                                                        new LcAnnotation(
                                                                                        "org.springframework.data.jpa.repository.Query",
                                                                                        Map.of("", value))),
                                                        "INT",
                                                        key,
                                                        List.of(
                                                                        new LcVariable("java.util.List<java.lang.String>",
                                                                                        "ids")),
                                                        List.of(),
                                                        null);
                                        repository.append(method);
                                });
        }
}
